#include <unordered_map>

#include "napi_utils.h"

#include "google/protobuf/message.h"
#include "google/protobuf/util/json_util.h"

namespace {
std::unordered_map<const google::protobuf::Arena *, size_t> arenaMap;
std::mutex arenaMtx;
thread_local std::unordered_map<std::string, napi_value> g_NS_MAP;
} // namespace

std::atomic_bool NapiUtils::g_ENABLE_ARENA = true;

napi_value NapiUtils::SetArenaEnabled(napi_env env, napi_callback_info info) {
    static std::atomic_bool hasRun = false;
    if (hasRun.exchange(true)) {
        OH_LOG_WARN(LOG_APP, "[NU] Arena state can only be set once, this invoke is ignored");
        return CreateUndefined(env);
    }
    auto args = GetArgs(env, info, 1);
    bool enable = NapiUtils::GetScalarValue<bool>(env, args[0]);
    g_ENABLE_ARENA.store(enable);
    OH_LOG_INFO(LOG_APP, "[NU] Set arena state to %{public}s", enable ? "enabled" : "disabled");
    return CreateUndefined(env);
}

#ifdef NAPI_UTILS_ENABLE_JSON
napi_value NapiUtils::ToJsonString(napi_env env, napi_callback_info info) {
    auto args = GetArgs(env, info, 1);
    if (auto impl = Unwrap<google::protobuf::Message>(env, args[0]); impl) {
        std::string json;
        if (google::protobuf::util::MessageToJsonString(*impl, &json).ok()) {
            return CreateScalarValue<std::string>(env, json);
        }
        OH_LOG_ERROR(LOG_APP, "[NU] Failed to serialize message to json string");
    }
    return CreateScalarValue<std::string>(env, "");
}
#endif

void NapiUtils::IncArenaRefCount(const google::protobuf::Arena *arena) {
    std::lock_guard<std::mutex> lock(arenaMtx);
    if (auto found = arenaMap.find(arena); found != arenaMap.end()) {
        ++found->second;
    } else {
        arenaMap.emplace(arena, 1);
    }
    OH_LOG_INFO(LOG_APP, "[NU] Increased arena 0x%{public}lx RC => %{public}lu", reinterpret_cast<uintptr_t>(arena),
                arenaMap[arena]);
}

void NapiUtils::DecArenaRefCount(const google::protobuf::Arena *arena) {
    std::lock_guard<std::mutex> lock(arenaMtx);
    if (auto found = arenaMap.find(arena); found != arenaMap.end()) {
        if (found->second > 1) {
            OH_LOG_INFO(LOG_APP, "[NU] Decrease arena 0x%{public}lx RC => %{public}lu", reinterpret_cast<uintptr_t>(arena),
                        found->second - 1);
            --found->second;
        } else {
            OH_LOG_INFO(LOG_APP, "[NU] Delete arena 0x%{public}lx", reinterpret_cast<uintptr_t>(arena));
            arenaMap.erase(arena);
            delete arena;
        }
    } else {
        OH_LOG_ERROR(LOG_APP, "[NU] Decreased arena 0x%{public}lx NOT FOUND", reinterpret_cast<uintptr_t>(arena));
    }
}

napi_value NapiUtils::GetThis(napi_env env, napi_callback_info info) {
    napi_value thisArg;
    auto status = napi_get_cb_info(env, info, nullptr, nullptr, &thisArg, nullptr);
    if (status != napi_ok) {
        OH_LOG_ERROR(LOG_APP, "[NU] Failed to get this");
        return CreateUndefined(env);
    }
    return thisArg;
}

std::vector<napi_value> NapiUtils::GetArgs(napi_env env, napi_callback_info info, size_t argc) {
    std::vector<napi_value> args(argc, nullptr);
    auto status = napi_get_cb_info(env, info, &argc, args.data(), nullptr, nullptr);
    if (status != napi_ok) {
        OH_LOG_ERROR(LOG_APP, "[NU] Failed to get args");
        return {};
    }
    args.resize(argc);
    return args;
}

std::tuple<napi_value, std::vector<napi_value>> NapiUtils::GetThisAndArgs(napi_env env, napi_callback_info info,
                                                                          size_t argc) {
    napi_value thisArg = nullptr;
    std::vector<napi_value> args(argc, nullptr);
    auto status = napi_get_cb_info(env, info, &argc, args.data(), &thisArg, nullptr);
    if (status != napi_ok) {
        OH_LOG_ERROR(LOG_APP, "[NU] Failed to get this and args");
        return {};
    }
    args.resize(argc);
    return {thisArg, std::move(args)};
}

napi_value NapiUtils::CreateUndefined(napi_env env) {
    napi_value undefined = nullptr;
    if (napi_get_undefined(env, &undefined) != napi_ok) {
        OH_LOG_ERROR(LOG_APP, "[NU] Failed to get undefined");
        return nullptr;
    }
    return undefined;
}

napi_value NapiUtils::CreateNull(napi_env env) {
    napi_value null = nullptr;
    if (napi_get_null(env, &null) != napi_ok) {
        OH_LOG_ERROR(LOG_APP, "[NU] Failed to get null");
        return nullptr;
    }
    return null;
}

bool NapiUtils::IsNull(napi_env env, napi_value value) {
    napi_valuetype type = napi_undefined;
    if (napi_typeof(env, value, &type) != napi_ok) {
        OH_LOG_ERROR(LOG_APP, "[NU] Failed to get value type");
        return false;
    }
    return type == napi_null;
}

bool NapiUtils::IsUndefined(napi_env env, napi_value value) {
    napi_valuetype type = napi_null;
    if (napi_typeof(env, value, &type) != napi_ok) {
        OH_LOG_ERROR(LOG_APP, "[NU] Failed to get value type");
        return false;
    }
    return type == napi_undefined;
}

std::optional<napi_value> NapiUtils::GetProperty(napi_env env, napi_value obj, const std::string &name) {
    napi_value result = nullptr;
    if (napi_get_named_property(env, obj, name.c_str(), &result) == napi_ok && result != nullptr &&
        !NapiUtils::IsUndefined(env, result) && !NapiUtils::IsNull(env, result)) {
        return {result};
    }
    return {};
}

void NapiUtils::SetNamespaceObj(const std::string &ns, const std::string &newNsObjName,const napi_value newNsObj) {
    OH_LOG_INFO(LOG_APP, "[NU] Set named property '%{public}s' in '%{public}s'", ns.c_str(), newNsObjName.c_str());
    g_NS_MAP.emplace(ns + "/" + newNsObjName, newNsObj);
}

napi_value NapiUtils::GetNamespaceObj(napi_env env, napi_value exports, const std::string &ns) {
    if (auto found = g_NS_MAP.find(ns); found != g_NS_MAP.end()) {
        return found->second;
    }
    napi_value outerNSObj = exports;
    std::string outerNS;
    auto found = std::find(ns.rbegin(), ns.rend(), '/');
    if (found != ns.rend()) {
        outerNS = ns.substr(0, ns.rend() - found - 1);
        outerNSObj = GetNamespaceObj(env, exports, outerNS);
    }
    if (!outerNSObj) {
        return nullptr;
    }
    auto curNS = ns.substr(ns.rend() - found);
    napi_value curNSObj = nullptr;
    if (napi_create_object(env, &curNSObj) == napi_ok) {
        OH_LOG_INFO(LOG_APP, "[NU] Created namespace '%{public}s' in '%{public}s'", curNS.c_str(), outerNS.c_str());
        if (napi_set_named_property(env, outerNSObj, curNS.c_str(), curNSObj) == napi_ok) {
            OH_LOG_INFO(LOG_APP, "[NU] Set named property '%{public}s' in '%{public}s'", curNS.c_str(), outerNS.c_str());
            g_NS_MAP.emplace(ns, curNSObj);
            return curNSObj;
        } else {
            OH_LOG_ERROR(LOG_APP, "[NU] Failed to set named property '%{public}s' in '%{public}s'", curNS.c_str(), outerNS.c_str());
            return nullptr;
        }
    } else {
        OH_LOG_ERROR(LOG_APP, "[NU] Failed to create namespace '%{public}s' in '%{public}s'", curNS.c_str(), outerNS.c_str());
        return nullptr;
    }
}

napi_value NapiUtils::CreateUInt8Array(napi_env env, const char *data, size_t length) {
    napi_value buffer = nullptr;
    void *bufferData = nullptr;
    napi_status status = napi_create_arraybuffer(env, length, &bufferData, &buffer);
    if (status != napi_ok) {
        OH_LOG_ERROR(LOG_APP, "[NU] Failed to create arraybuffer from data");
        return CreateUndefined(env);
    }
    std::memcpy(bufferData, data, length);
    napi_value result = nullptr;
    status = napi_create_typedarray(env, napi_uint8_array, length, buffer, 0, &result);
    if (status != napi_ok) {
        OH_LOG_ERROR(LOG_APP, "[NU] Failed to create uint8array from arraybuffer");
        return CreateUndefined(env);
    }
    return result;
}

std::string NapiUtils::GetUInt8ArrayValue(napi_env env, napi_value value) {
    bool isTypedArray = false;
    napi_is_typedarray(env, value, &isTypedArray);
    if (!isTypedArray) {
        napi_throw_type_error(env, nullptr, "Expected Uint8Array");
        return {};
    }

    napi_typedarray_type type;
    size_t byteLength = 0;
    void *data = nullptr;

    if (auto status = napi_get_typedarray_info(env, value, &type, &byteLength, &data, nullptr, nullptr);
        status != napi_ok || type != napi_uint8_array) {
        OH_LOG_ERROR(LOG_APP,
                     "[NU] Failed to extract UInt8Array from napi_value, status: %{public}d, actual type: %{public}d",
                     status, type);
        return {};
    }

    return std::string(reinterpret_cast<const char *>(data), byteLength);
}

napi_value NapiUtils::CreateArray(napi_env env, const std::vector<napi_value> &elements) {
    napi_value result = nullptr;
    napi_status status = napi_create_array_with_length(env, elements.size(), &result);
    if (status != napi_ok) {
        OH_LOG_ERROR(LOG_APP, "[NU] Failed to create array from elements");
        return CreateUndefined(env);
    }
    for (size_t i = 0; i < elements.size(); ++i) {
        status = napi_set_element(env, result, i, elements[i]);
        if (status != napi_ok) {
            OH_LOG_ERROR(LOG_APP, "[NU] Failed to set element %{public}llu in array", i);
            return CreateUndefined(env);
        }
    }
    return result;
}

bool NapiUtils::GetInt64FromJsLong(napi_env env, napi_value value, int64_t *result) {
    napi_value _low = nullptr;
    napi_value _high = nullptr;
    napi_value _unsigned = nullptr;
    if (napi_get_named_property(env, value, "low", &_low) == napi_ok &&
        napi_get_named_property(env, value, "high", &_high) == napi_ok &&
        napi_get_named_property(env, value, "_unsigned", &_unsigned) == napi_ok) {

        int32_t low, high;
        if (napi_get_value_int32(env, _low, &low) == napi_ok &&
            napi_get_value_int32(env, _high, &high) == napi_ok) {
            /**
             * 对应 js 源码实现：
             *
             * if (this.unsigned)
             *   return ((this.high >>> 0) * TWO_PWR_32_DBL) + (this.low >>> 0);
             * return this.high * TWO_PWR_32_DBL + (this.low >>> 0);
             */
            *result = high * (1l<<32) + low;
            OH_LOG_INFO(LOG_APP, "[NU] Extracted Long %{public}ld", *result);
            return true;
        }
    }
    OH_LOG_ERROR(LOG_APP, "[NU] Failed to extract Long");
    return false;
}

napi_value NapiUtils::NewInstance(napi_env env, napi_ref ctorRef) {
    napi_value ctor = nullptr;
    napi_value thisArg = nullptr;
    napi_get_reference_value(env, ctorRef, &ctor);
    napi_value ctorArgs[1] = {NapiUtils::CreateNull(env)};
    napi_new_instance(env, ctor, 1, ctorArgs, &thisArg);
    return thisArg;
}

napi_value NapiUtils::FromBinary(napi_env env, napi_callback_info info, google::protobuf::MessageLite *impl,
                                 napi_ref ctorRef, napi_finalize finalizer) {
    auto args = NapiUtils::GetArgs(env, info, 1);
    napi_typedarray_type type;
    size_t length = 0;
    void *dataPtr = nullptr;
    napi_get_typedarray_info(env, args[0], &type, &length, &dataPtr, nullptr, nullptr);

    impl->ParseFromArray(dataPtr, length);
    napi_value thisArg = NewInstance(env, ctorRef);
    napi_wrap(env, thisArg, impl, finalizer, nullptr, nullptr);
    return thisArg;
}

napi_value NapiUtils::CreateArrayWithValues(napi_env env,
                                            const google::protobuf::RepeatedPtrField<std::string> &elements,
                                            bool isBytes) {
    napi_value result = nullptr;
    napi_status status = napi_create_array_with_length(env, elements.size(), &result);
    if (status != napi_ok) {
        OH_LOG_ERROR(LOG_APP, "[NU] Failed to create array from elements");
        return CreateUndefined(env);
    }
    for (size_t i = 0; i < elements.size(); ++i) {
        const auto &element = elements[i];
        status = napi_set_element(env, result, i,
                                  isBytes ? CreateUInt8Array(env, element.c_str(), element.size())
                                          : CreateScalarValue<std::string>(env, element));
        if (status != napi_ok) {
            OH_LOG_ERROR(LOG_APP, "[NU] Failed to set element %{public}llu in array", i);
            return CreateUndefined(env);
        }
    }
    return result;
}

void NapiUtils::SetFromArrayValue(napi_env env, google::protobuf::RepeatedPtrField<std::string> &repeated,
                                  napi_value array, bool isBytes) {
    repeated.Clear();
    uint32_t length = 0;
    napi_get_array_length(env, array, &length);
    for (uint32_t i = 0; i < length; ++i) {
        napi_value element = nullptr;
        napi_get_element(env, array, i, &element);
        repeated.Add(std::move(isBytes ? GetUInt8ArrayValue(env, element) : GetScalarValue<std::string>(env, element)));
    }
}
